package org.elasticsearch;

import com.obs.services.exception.ObsException;
import org.elasticsearch.cluster.metadata.RepositoryMetaData;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.env.Environment;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.plugins.ReloadablePlugin;
import org.elasticsearch.plugins.RepositoryPlugin;
import org.elasticsearch.repositories.Repository;
import org.elasticsearch.threadpool.ThreadPool;
import shade.fasterxml.jackson.databind.JsonNode;
import shade.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.*;

/**
 * A plugin to add a repository type that writes to and from the Huawei OBS.
 */
public class ObsRepositoryPlugin extends Plugin implements RepositoryPlugin, ReloadablePlugin {

    private static final ObjectMapper objectMapper = new ObjectMapper();

    protected final ObsService service;

    public ObsRepositoryPlugin(final Settings settings) {
        this(settings, new ObsService());
    }

    static {
        SpecialPermission.check();
        AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
            try {
                // kick jackson to do some static caching of declared members info
                fromJsonString("{}", JsonNode.class);
                // ClientConfiguration client has some classloader problems
                Class.forName("com.obs.services.ObsConfiguration");
            } catch (final ClassNotFoundException e) {
                throw new RuntimeException(e);
            }
            return null;
        });
    }

    /**
     * Returns the deserialized object from the given json string and target
     * class; or null if the given json string is null.
     */
    public static <T> T fromJsonString(String json, Class<T> clazz) {
        if (json == null) {
            return null;
        }
        try {
            return objectMapper.readValue(json, clazz);
        } catch (Exception e) {
            throw new ObsException("Unable to parse Json String.", e);
        }
    }



    ObsRepositoryPlugin(final Settings settings, final ObsService service) {
        this.service = Objects.requireNonNull(service, "obs service must not be null");
        final Map<String, ObsClientSettings> clientsSettings = ObsClientSettings.load(settings);
        this.service.refreshAndClearCache(clientsSettings);
    }


    @Override
    public Map<String, Repository.Factory> getRepositories(final Environment env,
                                                           final NamedXContentRegistry namedXContentRegistry,
                                                           final ThreadPool threadPool) {
        return Collections.singletonMap(ObsRepository.TYPE, metadata -> new ObsRepository(metadata, namedXContentRegistry,service, threadPool));
    }

    @Override
    public List<Setting<?>> getSettings() {
        return Arrays.asList(
                ObsClientSettings.ACCESS_KEY,
                ObsClientSettings.SECRET_ACCESS_KEY,
                ObsClientSettings.SECURITY_TOKEN,
                ObsClientSettings.ENDPOINT,
                ObsClientSettings.REGION,
                ObsClientSettings.BUCKET,
                ObsClientSettings.BASE_PATH,
                ObsClientSettings.PROXY_USERNAME_SETTING,
                ObsClientSettings.PROXY_PASSWORD_SETTING,
                ObsClientSettings.PROTOCOL_SETTING,
                ObsClientSettings.PROXY_HOST_SETTING,
                ObsClientSettings.PROXY_PORT_SETTING,
                ObsClientSettings.COMPRESS,
                ObsClientSettings.CHUNK_SIZE);
    }

    @Override
    public void reload(Settings settings) {
        final Map<String, ObsClientSettings> clientsSettings = ObsClientSettings.load(settings);
        service.refreshAndClearCache(clientsSettings);
    }

    @Override
    public void close() throws IOException {
        service.close();
    }
}
